resumable.js ➔ processCallbacks   A
last analyzed

Complexity

Conditions 3

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 5
dl 0
loc 10
rs 10
c 0
b 0
f 0
1
/*
2
* MIT Licensed
3
* http://www.23developer.com/opensource
4
* http://github.com/23/resumable.js
5
* Steffen Tiedemann Christensen, [email protected]
6
*/
7
8
(function(){
9
"use strict";
10
11
  var Resumable = function(opts){
12
    if ( !(this instanceof Resumable) ) {
13
      return new Resumable(opts);
14
    }
15
    this.version = 1.0;
16
    // SUPPORTED BY BROWSER?
17
    // Check if these features are support by the browser:
18
    // - File object type
19
    // - Blob object type
20
    // - FileList object type
21
    // - slicing files
22
    this.support = (
23
                   (typeof(File)!=='undefined')
24
                   &&
25
                   (typeof(Blob)!=='undefined')
26
                   &&
27
                   (typeof(FileList)!=='undefined')
28
                   &&
29
                   (!!Blob.prototype.webkitSlice||!!Blob.prototype.mozSlice||!!Blob.prototype.slice||false)
30
                   );
31
    if(!this.support) return(false);
32
33
34
    // PROPERTIES
35
    var $ = this;
36
    $.files = [];
37
    $.defaults = {
38
      chunkSize:1*1024*1024,
39
      forceChunkSize:false,
40
      simultaneousUploads:3,
41
      fileParameterName:'file',
42
      chunkNumberParameterName: 'resumableChunkNumber',
43
      chunkSizeParameterName: 'resumableChunkSize',
44
      currentChunkSizeParameterName: 'resumableCurrentChunkSize',
45
      totalSizeParameterName: 'resumableTotalSize',
46
      typeParameterName: 'resumableType',
47
      identifierParameterName: 'resumableIdentifier',
48
      fileNameParameterName: 'resumableFilename',
49
      relativePathParameterName: 'resumableRelativePath',
50
      totalChunksParameterName: 'resumableTotalChunks',
51
      dragOverClass: 'dragover',
52
      throttleProgressCallbacks: 0.5,
53
      query:{},
54
      headers:{},
55
      preprocess:null,
56
      preprocessFile:null,
57
      method:'multipart',
58
      uploadMethod: 'POST',
59
      testMethod: 'GET',
60
      prioritizeFirstAndLastChunk:false,
61
      target:'/',
62
      testTarget: null,
63
      parameterNamespace:'',
64
      testChunks:true,
65
      generateUniqueIdentifier:null,
66
      getTarget:null,
67
      maxChunkRetries:100,
68
      chunkRetryInterval:undefined,
69
      permanentErrors:[400, 401, 403, 404, 409, 415, 500, 501],
70
      maxFiles:undefined,
71
      withCredentials:false,
72
      xhrTimeout:0,
73
      clearInput:true,
74
      chunkFormat:'blob',
75
      setChunkTypeFromFile:false,
76
      maxFilesErrorCallback:function (files, errorCount) {
77
        var maxFiles = $.getOpt('maxFiles');
78
        alert('Please upload no more than ' + maxFiles + ' file' + (maxFiles === 1 ? '' : 's') + ' at a time.');
0 ignored issues
show
Debugging Code Best Practice introduced by
The alert UI element is often considered obtrusive and is generally only used as a temporary measure. Consider replacing it with another UI element.
Loading history...
79
      },
80
      minFileSize:1,
81
      minFileSizeErrorCallback:function(file, errorCount) {
82
        alert(file.fileName||file.name +' is too small, please upload files larger than ' + $h.formatSize($.getOpt('minFileSize')) + '.');
0 ignored issues
show
Debugging Code Best Practice introduced by
The alert UI element is often considered obtrusive and is generally only used as a temporary measure. Consider replacing it with another UI element.
Loading history...
83
      },
84
      maxFileSize:undefined,
85
      maxFileSizeErrorCallback:function(file, errorCount) {
86
        alert(file.fileName||file.name +' is too large, please upload files less than ' + $h.formatSize($.getOpt('maxFileSize')) + '.');
0 ignored issues
show
Debugging Code Best Practice introduced by
The alert UI element is often considered obtrusive and is generally only used as a temporary measure. Consider replacing it with another UI element.
Loading history...
87
      },
88
      fileType: [],
89
      fileTypeErrorCallback: function(file, errorCount) {
90
        alert(file.fileName||file.name +' has type not allowed, please upload files of type ' + $.getOpt('fileType') + '.');
0 ignored issues
show
Debugging Code Best Practice introduced by
The alert UI element is often considered obtrusive and is generally only used as a temporary measure. Consider replacing it with another UI element.
Loading history...
91
      }
92
    };
93
    $.opts = opts||{};
94
    $.getOpt = function(o) {
95
      var $opt = this;
96
      // Get multiple option if passed an array
97
      if(o instanceof Array) {
98
        var options = {};
99
        $h.each(o, function(option){
100
          options[option] = $opt.getOpt(option);
101
        });
102
        return options;
103
      }
104
      // Otherwise, just return a simple option
105
      if ($opt instanceof ResumableChunk) {
106
        if (typeof $opt.opts[o] !== 'undefined') { return $opt.opts[o]; }
107
        else { $opt = $opt.fileObj; }
0 ignored issues
show
Comprehensibility introduced by
else is not necessary here since all if branches return, consider removing it to reduce nesting and make code more readable.
Loading history...
108
      }
109
      if ($opt instanceof ResumableFile) {
110
        if (typeof $opt.opts[o] !== 'undefined') { return $opt.opts[o]; }
111
        else { $opt = $opt.resumableObj; }
0 ignored issues
show
Comprehensibility introduced by
else is not necessary here since all if branches return, consider removing it to reduce nesting and make code more readable.
Loading history...
112
      }
113
      if ($opt instanceof Resumable) {
114
        if (typeof $opt.opts[o] !== 'undefined') { return $opt.opts[o]; }
115
        else { return $opt.defaults[o]; }
0 ignored issues
show
Comprehensibility introduced by
else is not necessary here since all if branches return, consider removing it to reduce nesting and make code more readable.
Loading history...
116
      }
117
    };
118
    $.indexOf = function(array, obj) {
119
    	if (array.indexOf) { return array.indexOf(obj); }
120
    	for (var i = 0; i < array.length; i++) {
121
            if (array[i] === obj) { return i; }
122
        }
123
        return -1;
124
    }
125
126
    // EVENTS
127
    // catchAll(event, ...)
128
    // fileSuccess(file), fileProgress(file), fileAdded(file, event), filesAdded(files, filesSkipped), fileRetry(file),
129
    // fileError(file, message), complete(), progress(), error(message, file), pause()
130
    $.events = [];
131
    $.on = function(event,callback){
132
      $.events.push(event.toLowerCase(), callback);
133
    };
134
    $.fire = function(){
135
      // `arguments` is an object, not array, in FF, so:
136
      var args = [];
137
      for (var i=0; i<arguments.length; i++) args.push(arguments[i]);
138
      // Find event listeners, and support pseudo-event `catchAll`
139
      var event = args[0].toLowerCase();
140
      for (var i=0; i<=$.events.length; i+=2) {
141
        if($.events[i]==event) $.events[i+1].apply($,args.slice(1));
142
        if($.events[i]=='catchall') $.events[i+1].apply(null,args);
143
      }
144
      if(event=='fileerror') $.fire('error', args[2], args[1]);
145
      if(event=='fileprogress') $.fire('progress');
146
    };
147
148
149
    // INTERNAL HELPER METHODS (handy, but ultimately not part of uploading)
150
    var $h = {
151
      stopEvent: function(e){
152
        e.stopPropagation();
153
        e.preventDefault();
154
      },
155
      each: function(o,callback){
156
        if(typeof(o.length)!=='undefined') {
157
          for (var i=0; i<o.length; i++) {
158
            // Array or FileList
159
            if(callback(o[i])===false) return;
160
          }
161
        } else {
162
          for (i in o) {
163
            // Object
164
            if(callback(i,o[i])===false) return;
165
          }
166
        }
167
      },
168
      generateUniqueIdentifier:function(file, event){
169
        var custom = $.getOpt('generateUniqueIdentifier');
170
        if(typeof custom === 'function') {
171
          return custom(file, event);
172
        }
173
        var relativePath = file.webkitRelativePath||file.relativePath||file.fileName||file.name; // Some confusion in different versions of Firefox
174
        var size = file.size;
175
        return(size + '-' + relativePath.replace(/[^0-9a-zA-Z_-]/img, ''));
176
      },
177
      contains:function(array,test) {
178
        var result = false;
179
180
        $h.each(array, function(value) {
181
          if (value == test) {
182
            result = true;
183
            return false;
184
          }
185
          return true;
186
        });
187
188
        return result;
189
      },
190
      formatSize:function(size){
191
        if(size<1024) {
192
          return size + ' bytes';
193
        } else if(size<1024*1024) {
194
          return (size/1024.0).toFixed(0) + ' KB';
195
        } else if(size<1024*1024*1024) {
196
          return (size/1024.0/1024.0).toFixed(1) + ' MB';
197
        } else {
198
          return (size/1024.0/1024.0/1024.0).toFixed(1) + ' GB';
199
        }
200
      },
201
      getTarget:function(request, params){
202
        var target = $.getOpt('target');
203
204
        if (request === 'test' && $.getOpt('testTarget')) {
205
          target = $.getOpt('testTarget') === '/' ? $.getOpt('target') : $.getOpt('testTarget');
206
        }
207
208
        if (typeof target === 'function') {
209
          return target(params);
210
        }
211
212
        var separator = target.indexOf('?') < 0 ? '?' : '&';
213
        var joinedParams = params.join('&');
214
215
        if (joinedParams) target = target + separator + joinedParams;
216
217
        return target;
218
      }
219
    };
220
221
    var onDrop = function(e){
222
      e.currentTarget.classList.remove($.getOpt('dragOverClass'));
223
      $h.stopEvent(e);
224
225
      //handle dropped things as items if we can (this lets us deal with folders nicer in some cases)
226
      if (e.dataTransfer && e.dataTransfer.items) {
227
        loadFiles(e.dataTransfer.items, e);
228
      }
229
      //else handle them as files
230
      else if (e.dataTransfer && e.dataTransfer.files) {
231
        loadFiles(e.dataTransfer.files, e);
232
      }
233
    };
234
    var onDragLeave = function(e){
235
      e.currentTarget.classList.remove($.getOpt('dragOverClass'));
236
    };
237
    var onDragOverEnter = function(e) {
238
      e.preventDefault();
239
      var dt = e.dataTransfer;
240
      if ($.indexOf(dt.types, "Files") >= 0) { // only for file drop
241
        e.stopPropagation();
242
        dt.dropEffect = "copy";
243
        dt.effectAllowed = "copy";
244
        e.currentTarget.classList.add($.getOpt('dragOverClass'));
245
      } else { // not work on IE/Edge....
246
        dt.dropEffect = "none";
247
        dt.effectAllowed = "none";
248
      }
249
    };
250
251
    /**
252
     * processes a single upload item (file or directory)
253
     * @param {Object} item item to upload, may be file or directory entry
254
     * @param {string} path current file path
255
     * @param {File[]} items list of files to append new items to
256
     * @param {Function} cb callback invoked when item is processed
257
     */
258
    function processItem(item, path, items, cb) {
259
      var entry;
260
      if(item.isFile){
261
        // file provided
262
        return item.file(function(file){
263
          file.relativePath = path + file.name;
264
          items.push(file);
265
          cb();
266
        });
267
      }else if(item.isDirectory){
268
        // item is already a directory entry, just assign
269
        entry = item;
270
      }else if(item instanceof File) {
271
        items.push(item);
272
      }
273
      if('function' === typeof item.webkitGetAsEntry){
274
        // get entry from file object
275
        entry = item.webkitGetAsEntry();
276
      }
277
      if(entry && entry.isDirectory){
278
        // directory provided, process it
279
        return processDirectory(entry, path + entry.name + '/', items, cb);
280
      }
281
      if('function' === typeof item.getAsFile){
282
        // item represents a File object, convert it
283
        item = item.getAsFile();
284
        if(item instanceof File) {
285
          item.relativePath = path + item.name;
286
          items.push(item);
287
        }
288
      }
289
      cb(); // indicate processing is done
290
    }
291
292
293
    /**
294
     * cps-style list iteration.
295
     * invokes all functions in list and waits for their callback to be
296
     * triggered.
297
     * @param  {Function[]}   items list of functions expecting callback parameter
298
     * @param  {Function} cb    callback to trigger after the last callback has been invoked
299
     */
300
    function processCallbacks(items, cb){
301
      if(!items || items.length === 0){
302
        // empty or no list, invoke callback
303
        return cb();
304
      }
305
      // invoke current function, pass the next part as continuation
306
      items[0](function(){
307
        processCallbacks(items.slice(1), cb);
308
      });
309
    }
310
311
    /**
312
     * recursively traverse directory and collect files to upload
313
     * @param  {Object}   directory directory to process
314
     * @param  {string}   path      current path
315
     * @param  {File[]}   items     target list of items
316
     * @param  {Function} cb        callback invoked after traversing directory
317
     */
318
    function processDirectory (directory, path, items, cb) {
319
      var dirReader = directory.createReader();
320
      var allEntries = [];
321
322
      function readEntries () {
323
        dirReader.readEntries(function(entries){
324
          if (entries.length) {
325
            allEntries = allEntries.concat(entries);
326
            return readEntries();
327
          }
328
329
          // process all conversion callbacks, finally invoke own one
330
          processCallbacks(
331
            allEntries.map(function(entry){
332
              // bind all properties except for callback
333
              return processItem.bind(null, entry, path, items);
0 ignored issues
show
Unused Code introduced by
The call to bind does not seem necessary since the function processItem declared on line 258 does not use this.
Loading history...
334
            }),
335
            cb
336
          );
337
        });
338
      }
339
340
      readEntries();
341
    }
342
343
    /**
344
     * process items to extract files to be uploaded
345
     * @param  {File[]} items items to process
346
     * @param  {Event} event event that led to upload
347
     */
348
    function loadFiles(items, event) {
349
      if(!items.length){
350
        return; // nothing to do
351
      }
352
      $.fire('beforeAdd');
353
      var files = [];
354
      processCallbacks(
355
          Array.prototype.map.call(items, function(item){
356
            // bind all properties except for callback
357
            var entry = item;
358
            if('function' === typeof item.webkitGetAsEntry){
359
              entry = item.webkitGetAsEntry();
360
            }
361
            return processItem.bind(null, entry, "", files);
0 ignored issues
show
Unused Code introduced by
The call to bind does not seem necessary since the function processItem declared on line 258 does not use this.
Loading history...
362
          }),
363
          function(){
364
            if(files.length){
365
              // at least one file found
366
              appendFilesFromFileList(files, event);
367
            }
368
          }
369
      );
370
    };
371
372
    var appendFilesFromFileList = function(fileList, event){
373
      // check for uploading too many files
374
      var errorCount = 0;
375
      var o = $.getOpt(['maxFiles', 'minFileSize', 'maxFileSize', 'maxFilesErrorCallback', 'minFileSizeErrorCallback', 'maxFileSizeErrorCallback', 'fileType', 'fileTypeErrorCallback']);
376
      if (typeof(o.maxFiles)!=='undefined' && o.maxFiles<(fileList.length+$.files.length)) {
377
        // if single-file upload, file is already added, and trying to add 1 new file, simply replace the already-added file
378
        if (o.maxFiles===1 && $.files.length===1 && fileList.length===1) {
379
          $.removeFile($.files[0]);
380
        } else {
381
          o.maxFilesErrorCallback(fileList, errorCount++);
382
          return false;
383
        }
384
      }
385
      var files = [], filesSkipped = [], remaining = fileList.length;
386
      var decreaseReamining = function(){
387
        if(!--remaining){
388
          // all files processed, trigger event
389
          if(!files.length && !filesSkipped.length){
390
            // no succeeded files, just skip
391
            return;
392
          }
393
          window.setTimeout(function(){
394
            $.fire('filesAdded', files, filesSkipped);
395
          },0);
396
        }
397
      };
398
      $h.each(fileList, function(file){
399
        var fileName = file.name;
400
        var fileType = file.type; // e.g video/mp4
401
        if(o.fileType.length > 0){
402
          var fileTypeFound = false;
403
          for(var index in o.fileType){
404
            // For good behaviour we do some inital sanitizing. Remove spaces and lowercase all
405
            o.fileType[index] = o.fileType[index].replace(/\s/g, '').toLowerCase();
406
407
            // Allowing for both [extension, .extension, mime/type, mime/*]
408
            var extension = ((o.fileType[index].match(/^[^.][^/]+$/)) ? '.' : '') + o.fileType[index];
409
410
            if ((fileName.substr(-1 * extension.length).toLowerCase() === extension) ||
411
              //If MIME type, check for wildcard or if extension matches the files tiletype
412
              (extension.indexOf('/') !== -1 && (
413
                (extension.indexOf('*') !== -1 && fileType.substr(0, extension.indexOf('*')) === extension.substr(0, extension.indexOf('*'))) ||
414
                fileType === extension
415
              ))
416
            ){
417
              fileTypeFound = true;
418
              break;
419
            }
420
          }
421
          if (!fileTypeFound) {
422
            o.fileTypeErrorCallback(file, errorCount++);
423
            return true;
424
          }
425
        }
426
427
        if (typeof(o.minFileSize)!=='undefined' && file.size<o.minFileSize) {
428
          o.minFileSizeErrorCallback(file, errorCount++);
429
          return true;
430
        }
431
        if (typeof(o.maxFileSize)!=='undefined' && file.size>o.maxFileSize) {
432
          o.maxFileSizeErrorCallback(file, errorCount++);
433
          return true;
434
        }
435
436
        function addFile(uniqueIdentifier){
437
          if (!$.getFromUniqueIdentifier(uniqueIdentifier)) {(function(){
438
            file.uniqueIdentifier = uniqueIdentifier;
439
            var f = new ResumableFile($, file, uniqueIdentifier);
440
            $.files.push(f);
441
            files.push(f);
442
            f.container = (typeof event != 'undefined' ? event.srcElement : null);
443
            window.setTimeout(function(){
444
              $.fire('fileAdded', f, event)
445
            },0);
446
          })()} else {
447
            filesSkipped.push(file);
448
          };
449
          decreaseReamining();
450
        }
451
        // directories have size == 0
452
        var uniqueIdentifier = $h.generateUniqueIdentifier(file, event);
453
        if(uniqueIdentifier && typeof uniqueIdentifier.then === 'function'){
454
          // Promise or Promise-like object provided as unique identifier
455
          uniqueIdentifier
456
          .then(
457
            function(uniqueIdentifier){
458
              // unique identifier generation succeeded
459
              addFile(uniqueIdentifier);
460
            },
461
           function(){
462
              // unique identifier generation failed
463
              // skip further processing, only decrease file count
464
              decreaseReamining();
465
            }
466
          );
467
        }else{
468
          // non-Promise provided as unique identifier, process synchronously
469
          addFile(uniqueIdentifier);
470
        }
471
      });
472
    };
473
474
    // INTERNAL OBJECT TYPES
475
    function ResumableFile(resumableObj, file, uniqueIdentifier){
476
      var $ = this;
477
      $.opts = {};
478
      $.getOpt = resumableObj.getOpt;
479
      $._prevProgress = 0;
480
      $.resumableObj = resumableObj;
481
      $.file = file;
482
      $.fileName = file.fileName||file.name; // Some confusion in different versions of Firefox
483
      $.size = file.size;
484
      $.relativePath = file.relativePath || file.webkitRelativePath || $.fileName;
485
      $.uniqueIdentifier = uniqueIdentifier;
486
      $._pause = false;
487
      $.container = '';
488
      $.preprocessState = 0; // 0 = unprocessed, 1 = processing, 2 = finished
489
      var _error = uniqueIdentifier !== undefined;
490
491
      // Callback when something happens within the chunk
492
      var chunkEvent = function(event, message){
493
        // event can be 'progress', 'success', 'error' or 'retry'
494
        switch(event){
495
        case 'progress':
496
          $.resumableObj.fire('fileProgress', $, message);
497
          break;
498
        case 'error':
499
          $.abort();
500
          _error = true;
501
          $.chunks = [];
502
          $.resumableObj.fire('fileError', $, message);
503
          break;
504
        case 'success':
505
          if(_error) return;
506
          $.resumableObj.fire('fileProgress', $, message); // it's at least progress
507
          if($.isComplete()) {
508
            $.resumableObj.fire('fileSuccess', $, message);
509
          }
510
          break;
511
        case 'retry':
512
          $.resumableObj.fire('fileRetry', $);
513
          break;
514
        }
515
      };
516
517
      // Main code to set up a file object with chunks,
518
      // packaged to be able to handle retries if needed.
519
      $.chunks = [];
520
      $.abort = function(){
521
        // Stop current uploads
522
        var abortCount = 0;
523
        $h.each($.chunks, function(c){
524
          if(c.status()=='uploading') {
525
            c.abort();
526
            abortCount++;
527
          }
528
        });
529
        if(abortCount>0) $.resumableObj.fire('fileProgress', $);
530
      };
531
      $.cancel = function(){
532
        // Reset this file to be void
533
        var _chunks = $.chunks;
534
        $.chunks = [];
535
        // Stop current uploads
536
        $h.each(_chunks, function(c){
537
          if(c.status()=='uploading')  {
538
            c.abort();
539
            $.resumableObj.uploadNextChunk();
540
          }
541
        });
542
        $.resumableObj.removeFile($);
543
        $.resumableObj.fire('fileProgress', $);
544
      };
545
      $.retry = function(){
546
        $.bootstrap();
547
        var firedRetry = false;
548
        $.resumableObj.on('chunkingComplete', function(){
549
          if(!firedRetry) $.resumableObj.upload();
550
          firedRetry = true;
551
        });
552
      };
553
      $.bootstrap = function(){
554
        $.abort();
555
        _error = false;
556
        // Rebuild stack of chunks from file
557
        $.chunks = [];
558
        $._prevProgress = 0;
559
        var round = $.getOpt('forceChunkSize') ? Math.ceil : Math.floor;
560
        var maxOffset = Math.max(round($.file.size/$.getOpt('chunkSize')),1);
561
        for (var offset=0; offset<maxOffset; offset++) {(function(offset){
562
            window.setTimeout(function(){
563
                $.chunks.push(new ResumableChunk($.resumableObj, $, offset, chunkEvent));
564
                $.resumableObj.fire('chunkingProgress',$,offset/maxOffset);
565
            },0);
566
        })(offset)}
567
        window.setTimeout(function(){
568
            $.resumableObj.fire('chunkingComplete',$);
569
        },0);
570
      };
571
      $.progress = function(){
572
        if(_error) return(1);
573
        // Sum up progress across everything
574
        var ret = 0;
575
        var error = false;
576
        $h.each($.chunks, function(c){
577
          if(c.status()=='error') error = true;
578
          ret += c.progress(true); // get chunk progress relative to entire file
579
        });
580
        ret = (error ? 1 : (ret>0.99999 ? 1 : ret));
581
        ret = Math.max($._prevProgress, ret); // We don't want to lose percentages when an upload is paused
582
        $._prevProgress = ret;
583
        return(ret);
584
      };
585
      $.isUploading = function(){
586
        var uploading = false;
587
        $h.each($.chunks, function(chunk){
588
          if(chunk.status()=='uploading') {
589
            uploading = true;
590
            return(false);
591
          }
592
        });
593
        return(uploading);
594
      };
595
      $.isComplete = function(){
596
        var outstanding = false;
597
        if ($.preprocessState === 1) {
598
          return(false);
599
        }
600
        $h.each($.chunks, function(chunk){
601
          var status = chunk.status();
602
          if(status=='pending' || status=='uploading' || chunk.preprocessState === 1) {
603
            outstanding = true;
604
            return(false);
605
          }
606
        });
607
        return(!outstanding);
608
      };
609
      $.pause = function(pause){
610
          if(typeof(pause)==='undefined'){
611
              $._pause = ($._pause ? false : true);
612
          }else{
613
              $._pause = pause;
614
          }
615
      };
616
      $.isPaused = function() {
617
        return $._pause;
618
      };
619
      $.preprocessFinished = function(){
620
        $.preprocessState = 2;
621
        $.upload();
622
      };
623
      $.upload = function () {
624
        var found = false;
625
        if ($.isPaused() === false) {
626
          var preprocess = $.getOpt('preprocessFile');
627
          if(typeof preprocess === 'function') {
628
            switch($.preprocessState) {
629
            case 0: $.preprocessState = 1; preprocess($); return(true);
630
            case 1: return(true);
631
            case 2: break;
632
            }
633
          }
634
          $h.each($.chunks, function (chunk) {
635
            if (chunk.status() == 'pending' && chunk.preprocessState !== 1) {
636
              chunk.send();
637
              found = true;
638
              return(false);
639
            }
640
          });
641
        }
642
        return(found);
643
      }
644
      $.markChunksCompleted = function (chunkNumber) {
645
        if (!$.chunks || $.chunks.length <= chunkNumber) {
646
            return;
647
        }
648
        for (var num = 0; num < chunkNumber; num++) {
649
            $.chunks[num].markComplete = true;
650
        }
651
      };
652
653
      // Bootstrap and return
654
      $.resumableObj.fire('chunkingStart', $);
655
      $.bootstrap();
656
      return(this);
657
    }
658
659
660
    function ResumableChunk(resumableObj, fileObj, offset, callback){
661
      var $ = this;
662
      $.opts = {};
663
      $.getOpt = resumableObj.getOpt;
664
      $.resumableObj = resumableObj;
665
      $.fileObj = fileObj;
666
      $.fileObjSize = fileObj.size;
667
      $.fileObjType = fileObj.file.type;
668
      $.offset = offset;
669
      $.callback = callback;
670
      $.lastProgressCallback = (new Date);
671
      $.tested = false;
672
      $.retries = 0;
673
      $.pendingRetry = false;
674
      $.preprocessState = 0; // 0 = unprocessed, 1 = processing, 2 = finished
675
      $.markComplete = false;
676
677
      // Computed properties
678
      var chunkSize = $.getOpt('chunkSize');
679
      $.loaded = 0;
680
      $.startByte = $.offset*chunkSize;
681
      $.endByte = Math.min($.fileObjSize, ($.offset+1)*chunkSize);
682
      if ($.fileObjSize-$.endByte < chunkSize && !$.getOpt('forceChunkSize')) {
683
        // The last chunk will be bigger than the chunk size, but less than 2*chunkSize
684
        $.endByte = $.fileObjSize;
685
      }
686
      $.xhr = null;
687
688
      // test() makes a GET request without any data to see if the chunk has already been uploaded in a previous session
689
      $.test = function(){
690
        // Set up request and listen for event
691
        $.xhr = new XMLHttpRequest();
692
693
        var testHandler = function(e){
694
          $.tested = true;
695
          var status = $.status();
696
          if(status=='success') {
697
            $.callback(status, $.message());
698
            $.resumableObj.uploadNextChunk();
699
          } else {
700
            $.send();
701
          }
702
        };
703
        $.xhr.addEventListener('load', testHandler, false);
704
        $.xhr.addEventListener('error', testHandler, false);
705
        $.xhr.addEventListener('timeout', testHandler, false);
706
707
        // Add data from the query options
708
        var params = [];
709
        var parameterNamespace = $.getOpt('parameterNamespace');
710
        var customQuery = $.getOpt('query');
711
        if(typeof customQuery == 'function') customQuery = customQuery($.fileObj, $);
712
        $h.each(customQuery, function(k,v){
713
          params.push([encodeURIComponent(parameterNamespace+k), encodeURIComponent(v)].join('='));
714
        });
715
        // Add extra data to identify chunk
716
        params = params.concat(
717
          [
718
            // define key/value pairs for additional parameters
719
            ['chunkNumberParameterName', $.offset + 1],
720
            ['chunkSizeParameterName', $.getOpt('chunkSize')],
721
            ['currentChunkSizeParameterName', $.endByte - $.startByte],
722
            ['totalSizeParameterName', $.fileObjSize],
723
            ['typeParameterName', $.fileObjType],
724
            ['identifierParameterName', $.fileObj.uniqueIdentifier],
725
            ['fileNameParameterName', $.fileObj.fileName],
726
            ['relativePathParameterName', $.fileObj.relativePath],
727
            ['totalChunksParameterName', $.fileObj.chunks.length]
728
          ].filter(function(pair){
729
            // include items that resolve to truthy values
730
            // i.e. exclude false, null, undefined and empty strings
731
            return $.getOpt(pair[0]);
732
          })
733
          .map(function(pair){
734
            // map each key/value pair to its final form
735
            return [
736
              parameterNamespace + $.getOpt(pair[0]),
737
              encodeURIComponent(pair[1])
738
            ].join('=');
739
          })
740
        );
741
        // Append the relevant chunk and send it
742
        $.xhr.open($.getOpt('testMethod'), $h.getTarget('test', params));
743
        $.xhr.timeout = $.getOpt('xhrTimeout');
744
        $.xhr.withCredentials = $.getOpt('withCredentials');
745
        // Add data from header options
746
        var customHeaders = $.getOpt('headers');
747
        if(typeof customHeaders === 'function') {
748
          customHeaders = customHeaders($.fileObj, $);
749
        }
750
        $h.each(customHeaders, function(k,v) {
751
          $.xhr.setRequestHeader(k, v);
752
        });
753
        $.xhr.send(null);
754
      };
755
756
      $.preprocessFinished = function(){
757
        $.preprocessState = 2;
758
        $.send();
759
      };
760
761
      // send() uploads the actual data in a POST call
762
      $.send = function(){
763
        var preprocess = $.getOpt('preprocess');
764
        if(typeof preprocess === 'function') {
765
          switch($.preprocessState) {
766
          case 0: $.preprocessState = 1; preprocess($); return;
767
          case 1: return;
768
          case 2: break;
769
          }
770
        }
771
        if($.getOpt('testChunks') && !$.tested) {
772
          $.test();
773
          return;
774
        }
775
776
        // Set up request and listen for event
777
        $.xhr = new XMLHttpRequest();
778
779
        // Progress
780
        $.xhr.upload.addEventListener('progress', function(e){
781
          if( (new Date) - $.lastProgressCallback > $.getOpt('throttleProgressCallbacks') * 1000 ) {
782
            $.callback('progress');
783
            $.lastProgressCallback = (new Date);
784
          }
785
          $.loaded=e.loaded||0;
786
        }, false);
787
        $.loaded = 0;
788
        $.pendingRetry = false;
789
        $.callback('progress');
790
791
        // Done (either done, failed or retry)
792
        var doneHandler = function(e){
793
          var status = $.status();
794
          if(status=='success'||status=='error') {
795
            $.callback(status, $.message());
796
            $.resumableObj.uploadNextChunk();
797
          } else {
798
            $.callback('retry', $.message());
799
            $.abort();
800
            $.retries++;
801
            var retryInterval = $.getOpt('chunkRetryInterval');
802
            if(retryInterval !== undefined) {
803
              $.pendingRetry = true;
804
              setTimeout($.send, retryInterval);
805
            } else {
806
              $.send();
807
            }
808
          }
809
        };
810
        $.xhr.addEventListener('load', doneHandler, false);
811
        $.xhr.addEventListener('error', doneHandler, false);
812
        $.xhr.addEventListener('timeout', doneHandler, false);
813
814
        // Set up the basic query data from Resumable
815
        var query = [
816
          ['chunkNumberParameterName', $.offset + 1],
817
          ['chunkSizeParameterName', $.getOpt('chunkSize')],
818
          ['currentChunkSizeParameterName', $.endByte - $.startByte],
819
          ['totalSizeParameterName', $.fileObjSize],
820
          ['typeParameterName', $.fileObjType],
821
          ['identifierParameterName', $.fileObj.uniqueIdentifier],
822
          ['fileNameParameterName', $.fileObj.fileName],
823
          ['relativePathParameterName', $.fileObj.relativePath],
824
          ['totalChunksParameterName', $.fileObj.chunks.length],
825
        ].filter(function(pair){
826
          // include items that resolve to truthy values
827
          // i.e. exclude false, null, undefined and empty strings
828
          return $.getOpt(pair[0]);
829
        })
830
        .reduce(function(query, pair){
831
          // assign query key/value
832
          query[$.getOpt(pair[0])] = pair[1];
833
          return query;
834
        }, {});
835
        // Mix in custom data
836
        var customQuery = $.getOpt('query');
837
        if(typeof customQuery == 'function') customQuery = customQuery($.fileObj, $);
838
        $h.each(customQuery, function(k,v){
839
          query[k] = v;
840
        });
841
842
        var func = ($.fileObj.file.slice ? 'slice' : ($.fileObj.file.mozSlice ? 'mozSlice' : ($.fileObj.file.webkitSlice ? 'webkitSlice' : 'slice')));
843
        var bytes = $.fileObj.file[func]($.startByte, $.endByte, $.getOpt('setChunkTypeFromFile') ? $.fileObj.file.type : "");
844
        var data = null;
845
        var params = [];
846
847
        var parameterNamespace = $.getOpt('parameterNamespace');
848
                if ($.getOpt('method') === 'octet') {
849
                    // Add data from the query options
850
                    data = bytes;
851
                    $h.each(query, function (k, v) {
852
                        params.push([encodeURIComponent(parameterNamespace + k), encodeURIComponent(v)].join('='));
853
                    });
854
                } else {
855
                    // Add data from the query options
856
                    data = new FormData();
857
                    $h.each(query, function (k, v) {
858
                        data.append(parameterNamespace + k, v);
859
                        params.push([encodeURIComponent(parameterNamespace + k), encodeURIComponent(v)].join('='));
860
                    });
861
                    if ($.getOpt('chunkFormat') == 'blob') {
862
                        data.append(parameterNamespace + $.getOpt('fileParameterName'), bytes, $.fileObj.fileName);
863
                    }
864
                    else if ($.getOpt('chunkFormat') == 'base64') {
865
                        var fr = new FileReader();
866
                        fr.onload = function (e) {
867
                            data.append(parameterNamespace + $.getOpt('fileParameterName'), fr.result);
868
                            $.xhr.send(data);
869
                        }
870
                        fr.readAsDataURL(bytes);
871
                    }
872
                }
873
874
        var target = $h.getTarget('upload', params);
875
        var method = $.getOpt('uploadMethod');
876
877
        $.xhr.open(method, target);
878
        if ($.getOpt('method') === 'octet') {
879
          $.xhr.setRequestHeader('Content-Type', 'application/octet-stream');
880
        }
881
        $.xhr.timeout = $.getOpt('xhrTimeout');
882
        $.xhr.withCredentials = $.getOpt('withCredentials');
883
        // Add data from header options
884
        var customHeaders = $.getOpt('headers');
885
        if(typeof customHeaders === 'function') {
886
          customHeaders = customHeaders($.fileObj, $);
887
        }
888
889
        $h.each(customHeaders, function(k,v) {
890
          $.xhr.setRequestHeader(k, v);
891
        });
892
893
        if ($.getOpt('chunkFormat') == 'blob') {
894
            $.xhr.send(data);
895
        }
896
      };
897
      $.abort = function(){
898
        // Abort and reset
899
        if($.xhr) $.xhr.abort();
900
        $.xhr = null;
901
      };
902
      $.status = function(){
903
        // Returns: 'pending', 'uploading', 'success', 'error'
904
        if($.pendingRetry) {
905
          // if pending retry then that's effectively the same as actively uploading,
906
          // there might just be a slight delay before the retry starts
907
          return('uploading');
908
        } else if($.markComplete) {
909
          return 'success';
910
        } else if(!$.xhr) {
911
          return('pending');
912
        } else if($.xhr.readyState<4) {
913
          // Status is really 'OPENED', 'HEADERS_RECEIVED' or 'LOADING' - meaning that stuff is happening
914
          return('uploading');
915
        } else {
916
          if($.xhr.status == 200 || $.xhr.status == 201) {
917
            // HTTP 200, 201 (created)
918
            return('success');
919
          } else if($h.contains($.getOpt('permanentErrors'), $.xhr.status) || $.retries >= $.getOpt('maxChunkRetries')) {
920
            // HTTP 400, 404, 409, 415, 500, 501 (permanent error)
921
            return('error');
922
          } else {
923
            // this should never happen, but we'll reset and queue a retry
924
            // a likely case for this would be 503 service unavailable
925
            $.abort();
926
            return('pending');
927
          }
928
        }
929
      };
930
      $.message = function(){
931
        return($.xhr ? $.xhr.responseText : '');
932
      };
933
      $.progress = function(relative){
934
        if(typeof(relative)==='undefined') relative = false;
935
        var factor = (relative ? ($.endByte-$.startByte)/$.fileObjSize : 1);
936
        if($.pendingRetry) return(0);
937
        if((!$.xhr || !$.xhr.status) && !$.markComplete) factor*=.95;
938
        var s = $.status();
939
        switch(s){
940
        case 'success':
941
        case 'error':
942
          return(1*factor);
943
        case 'pending':
944
          return(0*factor);
945
        default:
946
          return($.loaded/($.endByte-$.startByte)*factor);
947
        }
948
      };
949
      return(this);
950
    }
951
952
    // QUEUE
953
    $.uploadNextChunk = function(){
954
      var found = false;
955
956
      // In some cases (such as videos) it's really handy to upload the first
957
      // and last chunk of a file quickly; this let's the server check the file's
958
      // metadata and determine if there's even a point in continuing.
959
      if ($.getOpt('prioritizeFirstAndLastChunk')) {
960
        $h.each($.files, function(file){
961
          if(file.chunks.length && file.chunks[0].status()=='pending' && file.chunks[0].preprocessState === 0) {
962
            file.chunks[0].send();
963
            found = true;
964
            return(false);
965
          }
966
          if(file.chunks.length>1 && file.chunks[file.chunks.length-1].status()=='pending' && file.chunks[file.chunks.length-1].preprocessState === 0) {
967
            file.chunks[file.chunks.length-1].send();
968
            found = true;
969
            return(false);
970
          }
971
        });
972
        if(found) return(true);
973
      }
974
975
      // Now, simply look for the next, best thing to upload
976
      $h.each($.files, function(file){
977
        found = file.upload();
978
        if(found) return(false);
979
      });
980
      if(found) return(true);
981
982
      // The are no more outstanding chunks to upload, check is everything is done
983
      var outstanding = false;
984
      $h.each($.files, function(file){
985
        if(!file.isComplete()) {
986
          outstanding = true;
987
          return(false);
988
        }
989
      });
990
      if(!outstanding) {
991
        // All chunks have been uploaded, complete
992
        $.fire('complete');
993
      }
994
      return(false);
995
    };
996
997
998
    // PUBLIC METHODS FOR RESUMABLE.JS
999
    $.assignBrowse = function(domNodes, isDirectory){
1000
      if(typeof(domNodes.length)=='undefined') domNodes = [domNodes];
1001
      $h.each(domNodes, function(domNode) {
1002
        var input;
1003
        if(domNode.tagName==='INPUT' && domNode.type==='file'){
1004
          input = domNode;
1005
        } else {
1006
          input = document.createElement('input');
1007
          input.setAttribute('type', 'file');
1008
          input.style.display = 'none';
1009
          domNode.addEventListener('click', function(){
1010
            input.style.opacity = 0;
1011
            input.style.display='block';
1012
            input.focus();
1013
            input.click();
1014
            input.style.display='none';
1015
          }, false);
1016
          domNode.appendChild(input);
1017
        }
1018
        var maxFiles = $.getOpt('maxFiles');
1019
        if (typeof(maxFiles)==='undefined'||maxFiles!=1){
1020
          input.setAttribute('multiple', 'multiple');
1021
        } else {
1022
          input.removeAttribute('multiple');
1023
        }
1024
        if(isDirectory){
1025
          input.setAttribute('webkitdirectory', 'webkitdirectory');
1026
        } else {
1027
          input.removeAttribute('webkitdirectory');
1028
        }
1029
        var fileTypes = $.getOpt('fileType');
1030
        if (typeof (fileTypes) !== 'undefined' && fileTypes.length >= 1) {
1031
          input.setAttribute('accept', fileTypes.map(function (e) {
1032
            e = e.replace(/\s/g, '').toLowerCase();
1033
            if(e.match(/^[^.][^/]+$/)){
1034
              e = '.' + e;
1035
            }
1036
            return e;
1037
          }).join(','));
1038
        }
1039
        else {
1040
          input.removeAttribute('accept');
1041
        }
1042
        // When new files are added, simply append them to the overall list
1043
        input.addEventListener('change', function(e){
1044
          appendFilesFromFileList(e.target.files,e);
1045
          var clearInput = $.getOpt('clearInput');
1046
          if (clearInput) {
1047
            e.target.value = '';
1048
          }
1049
        }, false);
1050
      });
1051
    };
1052
    $.assignDrop = function(domNodes){
1053
      if(typeof(domNodes.length)=='undefined') domNodes = [domNodes];
1054
1055
      $h.each(domNodes, function(domNode) {
1056
        domNode.addEventListener('dragover', onDragOverEnter, false);
1057
        domNode.addEventListener('dragenter', onDragOverEnter, false);
1058
        domNode.addEventListener('dragleave', onDragLeave, false);
1059
        domNode.addEventListener('drop', onDrop, false);
1060
      });
1061
    };
1062
    $.unAssignDrop = function(domNodes) {
1063
      if (typeof(domNodes.length) == 'undefined') domNodes = [domNodes];
1064
1065
      $h.each(domNodes, function(domNode) {
1066
        domNode.removeEventListener('dragover', onDragOverEnter);
1067
        domNode.removeEventListener('dragenter', onDragOverEnter);
1068
        domNode.removeEventListener('dragleave', onDragLeave);
1069
        domNode.removeEventListener('drop', onDrop);
1070
      });
1071
    };
1072
    $.isUploading = function(){
1073
      var uploading = false;
1074
      $h.each($.files, function(file){
1075
        if (file.isUploading()) {
1076
          uploading = true;
1077
          return(false);
1078
        }
1079
      });
1080
      return(uploading);
1081
    };
1082
    $.upload = function(){
1083
      // Make sure we don't start too many uploads at once
1084
      if($.isUploading()) return;
1085
      // Kick off the queue
1086
      $.fire('uploadStart');
1087
      for (var num=1; num<=$.getOpt('simultaneousUploads'); num++) {
1088
        $.uploadNextChunk();
1089
      }
1090
    };
1091
    $.pause = function(){
1092
      // Resume all chunks currently being uploaded
1093
      $h.each($.files, function(file){
1094
        file.abort();
1095
      });
1096
      $.fire('pause');
1097
    };
1098
    $.cancel = function(){
1099
      $.fire('beforeCancel');
1100
      for(var i = $.files.length - 1; i >= 0; i--) {
1101
        $.files[i].cancel();
1102
      }
1103
      $.fire('cancel');
1104
    };
1105
    $.progress = function(){
1106
      var totalDone = 0;
1107
      var totalSize = 0;
1108
      // Resume all chunks currently being uploaded
1109
      $h.each($.files, function(file){
1110
        totalDone += file.progress()*file.size;
1111
        totalSize += file.size;
1112
      });
1113
      return(totalSize>0 ? totalDone/totalSize : 0);
1114
    };
1115
    $.addFile = function(file, event){
1116
      appendFilesFromFileList([file], event);
1117
    };
1118
    $.addFiles = function(files, event){
1119
      appendFilesFromFileList(files, event);
1120
    };
1121
    $.removeFile = function(file){
1122
      for(var i = $.files.length - 1; i >= 0; i--) {
1123
        if($.files[i] === file) {
1124
          $.files.splice(i, 1);
1125
        }
1126
      }
1127
    };
1128
    $.getFromUniqueIdentifier = function(uniqueIdentifier){
1129
      var ret = false;
1130
      $h.each($.files, function(f){
1131
        if(f.uniqueIdentifier==uniqueIdentifier) ret = f;
1132
      });
1133
      return(ret);
1134
    };
1135
    $.getSize = function(){
1136
      var totalSize = 0;
1137
      $h.each($.files, function(file){
1138
        totalSize += file.size;
1139
      });
1140
      return(totalSize);
1141
    };
1142
    $.handleDropEvent = function (e) {
1143
      onDrop(e);
1144
    };
1145
    $.handleChangeEvent = function (e) {
1146
      appendFilesFromFileList(e.target.files, e);
1147
      e.target.value = '';
1148
    };
1149
    $.updateQuery = function(query){
1150
        $.opts.query = query;
1151
    };
1152
1153
    return(this);
1154
  };
1155
1156
1157
  // Node.js-style export for Node and Component
1158
  if (typeof module != 'undefined') {
1159
    // left here for backwards compatibility
1160
    module.exports = Resumable;
1161
    module.exports.Resumable = Resumable;
1162
  } else if (typeof define === "function" && define.amd) {
1163
    // AMD/requirejs: Define the module
1164
    define(function(){
1165
      return Resumable;
1166
    });
1167
  } else {
1168
    // Browser: Expose to window
1169
    window.Resumable = Resumable;
1170
  }
1171
1172
})();